using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Threading;

/*
 * The protocol works like this:
 *  Commands:
 * Computer:
 * Send startbyte
 * Send packetID
 * Send commandtype
 * Send commandargs (optional)
 * Send stopbyte
 * Send checksum (sum of bytes from packetID to commandargs % 0xE0)
 * Arduino (subset of packet described below):
 * Respond startbyte
 * Respond packetID
 * Respond Response
 * Respond failcode
 * Respond packetID of packet responding to
 * Respond stopbyte
 * Respond checksum
 * 
 *  Data from Arduino
 * Arduino:
 * Send startbyte
 * Send packetID (independent of computer to arduino ids)
 * Send datatype
 * Send data (optional)
 * Send stopbyte
 * Respond checksum
*/

namespace PcArduinoControl
{
	class Protocol :
		IDisposable
	{
		const int MAX_COMMAND_SIZE = 7;

		public enum State
		{
			Starting,
			Started,
            Connected,
			Stopping,
			Stopped,
			Disposing,
			Disposed,
		}

		State mStatus;

		public State Status
		{
			get
			{
				return mStatus;
			}
			private set
			{
				mStatus = value;
				mLog.WriteLine("Protocol: Status changed to: " + mStatus.ToString());
			}
		}

		readonly object mControlLock = new object();
		readonly object mSendLock = new object();
		readonly object mReceiveLock = new object();

		string mPortName;
		SerialPort mPort;
		List<Exchange> mReceiveList = new List<Exchange>();

		byte mCurrentIncomingID;
		byte mCurrentOutgoingID = 0;

		// These event handlers start the event on a different thread so that delays will not cause a watchdog timeout
		public event ReceivedMessageHandler ReceivedMessage;
		public event ProtocolFailureHandler ProtocolFailure;

		Log mLog;
		SerialDataReceivedEventHandler receivedHandler;

		public Protocol(string port, Log log)
		{
			mPortName = port;
			mLog = log;
			receivedHandler = null;

			Status = State.Stopped;
		}

		#region Control

		public void Open()
		{
			lock (mControlLock)
			{
				if (Status == State.Started || Status == State.Connected)
					return;

				Status = State.Starting;
				// So the first number it looks for is 0.
				mCurrentIncomingID = Packet.cLimit - 1;
				try
				{
					mPort = new SerialPort(mPortName);
					receivedHandler = new SerialDataReceivedEventHandler(mPort_DataReceived);
					mPort.DataReceived += receivedHandler;
					mPort.DtrEnable = true;
					mPort.BaudRate = 57600;
					mPort.ReadTimeout = 100;
					mPort.WriteTimeout = 100;
					mPort.Open();
					Status = State.Started;
				}
				catch (Exception ex)
				{
					if (mPort != null)
						mPort.DataReceived -= receivedHandler;

					mPort.Dispose();
					mPort = null;
					Status = State.Stopped;
					throw ex;
				}
			}
		}

        // This is how Arduino tells Protocol we're connected, allows commands other than connect to be sent
        public void Connected()
        {
            Status = State.Connected;
        }

		public void Close()
		{
			lock (mControlLock)
			{
				if (Status == State.Stopped)
					return;

				Status = State.Stopping; // Will cause everything to stop

				Monitor.Enter(mReceiveLock);

				try
				{
					Monitor.Enter(mSendLock);

					try
					{
						try
						{
							if (receivedHandler != null)
							{
								mPort.DataReceived -= receivedHandler;
								receivedHandler = null;
							}

							mPort.Close();
							mPort = null;
						}
						catch (UnauthorizedAccessException uae)
						{
							mLog.WriteException(uae);
						}
						finally
						{
							Clear();
							Status = State.Stopped; // Will cause everything to stop
						}
					}
					finally
					{
						Monitor.Exit(mSendLock);
					}
				}
				finally
				{
					Monitor.Exit(mReceiveLock);
				}
			}
		}

		public void Dispose()
		{
			GC.SuppressFinalize(this);

			if (Status != State.Stopped)
				Close();

			lock (mControlLock)
			{
				Status = State.Disposing;
				try
				{
					mPort.Dispose();
				}
				catch (Exception) { }
				mPort = null;
				Status = State.Disposed;
			}
		}

		/// <summary>
		/// Don't use this unless you are connecting, or you like unhandled exceptions.
		/// </summary>
		public void Clear()
		{
			try
			{
				mPort.DiscardInBuffer();
			}
			catch (Exception) { }
			lock (mReceiveList)
				mReceiveList.Clear();
		}

		#endregion

		#region Send

		public Exchange SendCommand(Command command)
		{
			var exchange = new Exchange(command);

			if (Status == State.Connected || (command is CommandConnect && Status == State.Started))
			{
				Monitor.Enter(mSendLock);

				try
				{
					if (mPort != null && !mPort.IsOpen)
					{
						mLog.WriteLine("Error: Trying to send when port is not open!");
						Fail();
						return exchange;
					}

					lock (mReceiveList)
					{
						if (mReceiveList.Count > 128 / MAX_COMMAND_SIZE)
						{
							mLog.WriteLine("Too many commands being queued.  Arduino is getting overloaded.");
							mLog.WriteCommunicationsLogs();
							Fail();
							return exchange;
						}

						mReceiveList.Add(exchange);
					}

					bool sent = false;
					mCurrentOutgoingID = (byte)((mCurrentOutgoingID + 1) % Packet.cLimit);
					exchange.PacketID = mCurrentOutgoingID;
					byte[] buffer = exchange.mCommand.ToByteArray();

					sent = Write(buffer, 0, buffer.Length);

					if (sent)
					{
						exchange.SentAt = DateTime.UtcNow;
						exchange.IsSent = true;
						mLog.SerialLog.LogSent(buffer);
						mLog.PacketLog.LogPacket(exchange.mCommand);
					}
					else
						mLog.WriteLine("Error: Command could not be sent");
				}
				finally
				{
					Monitor.PulseAll(mSendLock);
					Monitor.Exit(mSendLock);
				}
			}
			else
				mLog.WriteLine("Not in correct state to send command.");

			return exchange;
		}

		void Resend(byte packetID)
		{
			lock (mReceiveList)
			{
				Exchange exchange = mReceiveList.Find(e => e.PacketID == packetID);
				if (exchange != null)
				{
					SendCommand(exchange.mCommand);
					mReceiveList.Remove(exchange);
				}
				else
					mLog.WriteLine("Could not resend packet because it was never sent or already got a response");
			}
		}

		bool Write(byte[] buffer, int offset, int count)
		{
			int tries = 0;
			bool successful = false;

			while (!successful && tries < 5)
			{
				successful = TryWrite(buffer, offset, count);
				tries++;
			}

			if (!successful)
			{
				mLog.WriteLine("Error: Too many unsuccessful writes.");
				Fail();
			}

			return successful;
		}

		bool TryWrite(byte[] buffer, int offset, int count)
		{
			try
			{
				lock (mPort)
					mPort.Write(buffer, offset, count);
			}
			catch (ThreadAbortException threadAbortException)
			{
				mLog.WriteLine(String.Format("Thread Abort Exception: {0}", Thread.CurrentThread.Name));
				mLog.WriteException(threadAbortException);
				throw;
			}
			catch (Exception e)
			{
				mLog.WriteException(e);
				return false;
			}
			return true;
		}

		#endregion

		#region Receive

        void mPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
			Monitor.Enter(mReceiveLock);

			try
			{
				HandleReceive();
			}
			finally
			{
				Monitor.PulseAll(mReceiveLock);
				Monitor.Exit(mReceiveLock);
			}
        }

		protected void HandleReceive()
		{
			while ((Status == State.Connected || Status == State.Started) && mPort.BytesToRead > 3)
			{
				while (ReadByte() != Packet.cStartByte) ;

				int index = -1;
				int checksum = 0;
				byte statedChecksum;
				byte[] messageBuffer = new byte[20];

				while (true)
				{
					index++;
					byte incoming = ReadByte();
					if (incoming == Packet.cTerminatorByte || index > 20)
						break;
					else
					{
						messageBuffer[index] = incoming;
						checksum = (checksum + incoming) % Packet.cLimit;
					}
				}

				statedChecksum = ReadByte();

				if (statedChecksum == checksum)
				{
					byte packetID = messageBuffer[0];
					if (packetID != (mCurrentIncomingID + 1) % Packet.cLimit)
					{
						if (packetID == (mCurrentIncomingID + 2) % Packet.cLimit)
							SendCommand(new CommandRequestResend(((byte)((mCurrentIncomingID + 1) % Packet.cLimit))));
						else if (packetID == (mCurrentIncomingID + 3) % Packet.cLimit)
						{
							SendCommand(new CommandRequestResend(((byte)((mCurrentIncomingID + 1) % Packet.cLimit))));
							SendCommand(new CommandRequestResend(((byte)((mCurrentIncomingID + 2) % Packet.cLimit))));
						}
						else
						{
                            mLog.WriteLine("Error: Too many missed packets. Expected ID " + (mCurrentIncomingID + 1) % Packet.cLimit + " got " + packetID);
							Fail();
						}
					}
					mCurrentIncomingID = packetID;

					MessageType messageType = (MessageType)messageBuffer[1];
					Message message;

					switch (messageType)
					{
						case MessageType.Response:
							if (index != 4)
							{
								mLog.WriteLine("Protocol: Incorrect number of arguments received.");
								mLog.WriteCommunicationsLogs();
							}

							ResponseMessage response = new ResponseMessage(packetID, statedChecksum, messageBuffer.Skip(2).Take(2));
							HandleResponse(response);
							message = response;
							break;
						case MessageType.WatchdogFeed:
							if (index != 2)
							{
								mLog.WriteLine("Protocol: Incorrect number of arguments received.");
								mLog.WriteCommunicationsLogs();
							}

							WatchdogFeedMessage watchdogFeed = new WatchdogFeedMessage(packetID, statedChecksum, messageBuffer.Skip(2).Take(0));
							message = watchdogFeed;
							break;
						case MessageType.DigitalInput:
							if (index != 4)
							{
								mLog.WriteLine("Protocol: Incorrect number of arguments received.");
								mLog.WriteCommunicationsLogs();
							}

							DigitalInputMessage digitalInput = new DigitalInputMessage(packetID, statedChecksum, messageBuffer.Skip(2).Take(2));
							message = digitalInput;
							break;
						case MessageType.AnalogInput:
							if (index != 4)
							{
								mLog.WriteLine("Protocol: Incorrect number of arguments received.");
								mLog.WriteCommunicationsLogs();
							}

							AnalogInputMessage analogInput = new AnalogInputMessage(packetID, statedChecksum, messageBuffer.Skip(2).Take(2));
							message = analogInput;
							break;
						case MessageType.Notification:
							if (index != 3)
							{
								mLog.WriteLine("Protocol: Incorrect number of arguments received.");
								mLog.WriteCommunicationsLogs();
							}

							NotificationMessage notification = new NotificationMessage(packetID, statedChecksum, messageBuffer.Skip(2).Take(1));
							message = notification;

							// Checksum will be further handled when a packet is shown missing.
							break;
						case MessageType.ResendRequest:
							if (index != 3)
							{
								mLog.WriteLine("Incorrect number of arguments received");
								mLog.WriteCommunicationsLogs();
							}

							ResendRequestMessage resendRequest = new ResendRequestMessage(packetID, statedChecksum, messageBuffer.Skip(2).Take(1));
							message = resendRequest;
							Resend(messageBuffer[2]);
							break;
						default:
							mLog.WriteLine("Protocol: Invalid message received");
							message = new Message(packetID, statedChecksum, MessageType.Invalid, messageBuffer);
							break;
					}

					mLog.PacketLog.LogPacket(message);
					OnReceivedMessage(message);
				}
				else
					mLog.WriteLine("Protocol: Checksum Error");  // We dont request resend here because we can't trust the packetID, we will request resend after we get the next packet anyway
			}
		}

		#region Read Methods

		protected byte ReadByte()
		{
			const int length = 1;
			byte[] buffer = new byte[length];
			Read(buffer, 0, length);
			mLog.SerialLog.LogReceived(buffer);
			return buffer[0];
		}

		protected sbyte ReadSByte()
		{
			const int length = 1;
			byte[] buffer = new byte[length];
			Read(buffer, 0, length);
			mLog.SerialLog.LogReceived(buffer);

			unchecked
			{
				return (sbyte)buffer[0];
			}
		}

		protected ushort ReadUShort()
		{
			const int length = 2;
			byte[] buffer = new byte[length];
			Read(buffer, 0, length);
			mLog.SerialLog.LogReceived(buffer);
			return BitConverter.ToUInt16(buffer, 0);
		}

		protected bool ReadBool()
		{
			const int length = 1;
			byte[] buffer = new byte[length];
			Read(buffer, 0, length);
			mLog.SerialLog.LogReceived(buffer);
			return BitConverter.ToBoolean(buffer, 0);
		}

		bool Read(byte[] buffer, int offset, int count)
		{
			int tries = 0;
			bool successful = false;

			while (!successful && tries < 5)
			{
				successful = TryRead(buffer, offset, count);
				tries++;
			}

			if (!successful)
			{
				mLog.WriteLine("Error: Too many unsuccessful reads.");
				Fail();
			}

			return successful;
		}

		bool TryRead(byte[] buffer, int offset, int count)
		{
			try
			{
				while (count > 0)
				{
					var length = 0;

					lock (mPort)
						length = mPort.Read(buffer, offset, count);

					offset += length;
					count -= length;
					if (count > 0)
						Thread.Yield();
				}
			}
			catch (Exception exception)
			{
				mLog.WriteException(exception);
				return false;
			}
			return true;
		}

		#endregion

		#region Process

		private void HandleResponse(ResponseMessage response)
		{
			Exchange exchange;

			lock (mReceiveList)
			{
				exchange = mReceiveList.Find(e => e.PacketID == response.CommandId);
				if (exchange != null)
					mReceiveList.Remove(exchange);
				else
				{
					mLog.WriteLine("Error: A response was received for a packet that was not in queue.");
					mLog.WriteCommunicationsLogs();
					return;
				}
			}

			exchange.FailCode = response.FailCode;
		}

		#region Event Triggerers

		protected void OnReceivedMessage(Message message)
		{
			var receivedMessage = ReceivedMessage;

			if (receivedMessage != null)
			{
				ThreadPool.QueueUserWorkItem((o) => receivedMessage(message));
			}
		}

		#endregion

		#endregion

		#endregion

		#region Failure Handling

		private void Fail()
		{
            if(Status == State.Connected) // So we don't try to restart if we're in the process of connecting
			    Close();
			OnProtocolFailure();
		}

		private void OnProtocolFailure()
		{
			var protocolFailure = ProtocolFailure;

			if (protocolFailure != null)
			{
				ThreadPool.QueueUserWorkItem((o) => protocolFailure());
			}
		}

		#endregion
	}
}
